/*
 * Tic-Tac-Toe - Turn-based strategy game
 * Copyright 2008, 2009 Shayne Riley and Paul Maseberg
 *
 * Tic-Tac-Toe is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tic-Tac-Toe is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Tic-Tac-Toe.  If not, see <http://www.gnu.org/licenses/>
 * or write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * To contact the authors on questions or issues regarding the license,
 * you may send an email to <shayner at gmail dot com>
 */

#include "Graphics.h"
#include <iostream>
#include <iomanip>
#include <sstream>
#include <cmath>

using namespace std;

Graphics::Graphics()
:
operational(false), sdlInited(false), shouldQuit(false), showFPS(false),
frameMillis(0), font(FTGLPixmapFont("res/DejaVuSansMono.ttf")), frameCount(0)
{
    if (initSDL())
    {
        sdlInited = true;

        if (initGL())
        {
            if (initGraphics())
            {
                operational = true;

                // If the font didn't load right then we aren't operational.
                FT_Error error = font.Error();
                if(error) {
                    cerr << "FreeType2 error code " << error << endl;
                    operational = false;
                }

                frameMillis = SDL_GetTicks();

                // Initialize the starting Control state.
                lastControl.mouseOn = SDL_GetAppState() & SDL_APPMOUSEFOCUS;
                int x = 0;
                int y = 0;
                Uint8 buttons = SDL_GetMouseState(&x, &y);
                lastControl.mouseX = (Uint16) x;
                lastControl.mouseY = (Uint16) y;
                lastControl.button0 = buttons & SDL_BUTTON(1);

                // Seed the random number generator. Why not?
                srand(SDL_GetTicks());
            }
        }
    }
}

Graphics::~Graphics()
{
    // Hey, bub. You clean up your own graphics. Use destroy() when you want
    // to stop using the graphics system.
}

void Graphics::destroy()
{
    if (sdlInited)
    {
        SDL_Quit();
    }
}

void Graphics::endFrame()
{
    // TODO Rewrite this so that it isn't dumb. I'm looking at you SHAYNE! BAH!
    uint32_t currentTicks = SDL_GetTicks();
    frameCount++;
    uint32_t diffTicks = currentTicks - frameMillis;
    float msPerFrame = (float) diffTicks / (float) frameCount;

    // TODO Oh, so you won't write anything if more than 1000FPS? 1 word: Silly.
    if (0 < diffTicks) {
        float fps = 1.0f / msPerFrame * 1000.0f;

        if (showFPS)
        {
            ostringstream oss;
            oss << showpoint << fixed << right;
            oss << setw(6) << setfill(' ') << setprecision(1) << fps << " FPS";
            string fpsText(oss.str());
            setFontSize(12);
            setColor(0, 0, 0, 1);
            drawText(fpsText.c_str(), 500, 450);
            oss.str(""); // clear the stream.

            oss << setw(6) << setfill(' ') << setprecision(1) << msPerFrame <<
            " ms/f";
            fpsText = oss.str();
            drawText(fpsText.c_str(), 500, 420);
        }

        // TODO WHY JUST REPORT THE CURRENT FPS? WHAT ABOUT THE 60 FRAME AVG?

        frameCount = 0;
        frameMillis = currentTicks;
    }

    SDL_GL_SwapBuffers();

    SDL_Delay(1);
}

void Graphics::addMouse(bool mouseOn, Uint16 mouseX, Uint16 mouseY,
        bool button0)
{
    Mouse mouse;

    mouse.mouseOn = mouseOn;
    mouse.mouseX = mouseX;
    mouse.mouseY = mouseY;
    mouse.button0 = button0;

    controls.push_back(mouse);

    lastControl = mouse;
}

void Graphics::consumeEvents()
{
    Uint16 maxYPos = (Uint16) (SDL_GetVideoSurface()->h - 1);
    controls.clear();
    bool mouseOn = lastControl.mouseOn;
    Uint16 mouseX = lastControl.mouseX;
    Uint16 mouseY = lastControl.mouseY;
    bool button0 = lastControl.button0;
    SDL_Event event;
    while (SDL_PollEvent(&event))
    {
        if (event.type == SDL_MOUSEBUTTONDOWN || event.type
                == SDL_MOUSEBUTTONUP)
        {
            if (event.button.button == 1)
            {
                mouseX = event.button.x;
                mouseY = (Uint16) (maxYPos - event.button.y); // Bottom-up man!
                button0 = event.button.state == SDL_PRESSED;

                addMouse(mouseOn, mouseX, mouseY, button0);
            }

        }
        else if (event.type == SDL_MOUSEMOTION)
        {
            mouseX = event.motion.x;
            mouseY = (Uint16) (maxYPos - event.button.y); // Bottom-up man!
            button0 = 0 < (event.motion.state & 0x1);

            addMouse(mouseOn, mouseX, mouseY, button0);
        }
        else if (event.type == SDL_ACTIVEEVENT)
        {
            // We only care about mouse focus (1).
            if (event.active.state == 1)
            {
                mouseOn = event.active.gain != 0;
                addMouse(mouseOn, mouseX, mouseY, button0);
            }
        }
        else if (event.type == SDL_QUIT)
        {
            shouldQuit = true;
        }
    }
}

bool Graphics::drawGame()
{
    consumeEvents();

    glClear(GL_COLOR_BUFFER_BIT);

    setColor(1, 1, 1, 1);
    drawTexture(&textureBack, 320, 240, 640, 480, 0.0f);

    return !shouldQuit;
}

void Graphics::drawTexture(Texture *texture, GLint leftX, GLint lowerY)
{
    // TODO must use glGet* before the glEnables, glDisables, and glBlendFunc
    // so that they can be properly restored later. Or, I could stop using
    // immediate mode which would make things safer and speed the whole thing up
    // even more.

    // Allow the alpha channel of the images to be used (or other blendings)
    glEnable(GL_BLEND);
    // Look at Ch. 6, pg. 234 Red Book for other options.
    // The bigger the source alpha channel, the more opaque it becomes.
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glEnable(GL_TEXTURE_2D);

    // Draw the O texture.
    glBindTexture(GL_TEXTURE_2D, texture->name);
    // Note: Normally this is called, but we do actually use the colors feature.
    // glColor4f(1, 1, 1, alpha); // Pure white so the texture colors are seen.
    glBegin(GL_QUADS);
    // lower-left
    glTexCoord2i(0, 0);
    glVertex2i(leftX, lowerY);
    // lower-right
    glTexCoord2i(1, 0);
    glVertex2i(leftX + texture->width, lowerY);
    // upper-left
    glTexCoord2i(1, 1);
    glVertex2i(leftX + texture->width, lowerY + texture->height);
    // upper-left
    glTexCoord2i(0, 1);
    glVertex2i(leftX, lowerY + texture->height);
    glEnd();

    glDisable(GL_TEXTURE_2D);
    glDisable(GL_BLEND);
}

void Graphics::drawTexture(Texture *texture, GLint centerX, GLint centerY,
        float angleDeg)
{
    drawTexture(texture, centerX, centerY, texture->width, texture->height,
            angleDeg);
}

void Graphics::drawTexture(Texture *texture, GLint centerX, GLint centerY,
        GLint width, GLint height, float angleDeg)
{
    int halfWidth = width / 2;
    int halfHeight = height / 2;

    // TODO must use glGet* before the glEnables, glDisables, and glBlendFunc
    // so that they can be properly restored later. Or, I could stop using
    // immediate mode which would make things safer and speed the whole thing up
    // even more.

    // Allow the alpha channel of the images to be used (or other blendings)
    glEnable(GL_BLEND);
    // Look at Ch. 6, pg. 234 Red Book for other options.
    // The bigger the source alpha channel, the more opaque it becomes.
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glEnable(GL_TEXTURE_2D);

    glPushMatrix();
    glLoadIdentity();
    glTranslatef((float) centerX, (float) centerY, 0.0f);
    glRotatef(angleDeg, 0.0f, 0.0f, 1.0f);

    // Draw the texture.
    glBindTexture(GL_TEXTURE_2D, texture->name);

    // Note: Normally this is called, but we do actually use the colors feature.
//    // Remember the old color and switch to the new one.
//    GLfloat oldColor[4];
//    glGetFloatv(GL_CURRENT_COLOR, oldColor);
//    glColor4f(1, 1, 1, alpha); // Pure white so the texture colors are seen.

    glBegin(GL_QUADS);
    // lower-left
    glTexCoord2i(0, 0);
    glVertex2i(-halfWidth, -halfHeight);
    // lower-right
    glTexCoord2i(1, 0);
    glVertex2i(halfWidth, -halfHeight);
    // upper-right
    glTexCoord2i(1, 1);
    glVertex2i(halfWidth, halfHeight);
    // upper-left
    glTexCoord2i(0, 1);
    glVertex2i(-halfWidth, halfHeight);
    glEnd();

//    // Restore the old color.
//    glColor4f(oldColor[0], oldColor[1], oldColor[2], oldColor[3]);

    glPopMatrix();

    glDisable(GL_TEXTURE_2D);
    glDisable(GL_BLEND);
}

void Graphics::drawText(const char *text, GLint leftX, GLint baselineY)
{
    font.Render(text, -1, FTPoint(leftX, baselineY), FTPoint(),
            FTGL::RENDER_ALL);
}

void Graphics::drawText(const char *text, GLint leftX, GLint lowerY,
        GLint width, GLint height)
{
    FTBBox textBox = font.BBox(text, -1, FTPoint(leftX, lowerY), FTPoint());

    float textWidthHalf = (textBox.Upper().Xf() - textBox.Lower().Xf()) / 2.0f;
    float boxWidthHalf = (float) width / 2.0f;
    float xPos = (float) leftX + boxWidthHalf - textWidthHalf;

    float lineHeightHalf = (textBox.Upper().Yf() - textBox.Lower().Yf()) / 2.0f;
    float boxHeightHalf = (float) height / 2.0f;
    float yPos = (float) lowerY + boxHeightHalf - lineHeightHalf;

    font.Render(text, -1, FTPoint(xPos, yPos), FTPoint(),
            FTGL::RENDER_ALL);
}

void Graphics::drawRect(GLint leftX, GLint lowerY, GLint width, GLint height)
{
    glEnable(GL_BLEND);

    // Look at Ch. 6, pg. 234 Red Book for other options.
    // The bigger the source alpha channel, the more opaque it becomes.
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glBegin(GL_LINE_LOOP);
    // lower-left
    glVertex2i(leftX, lowerY);
    // lower-right
    glVertex2i(leftX + width, lowerY);
    // upper-right
    glVertex2i(leftX + width, lowerY + height);
    // upper-left
    glVertex2i(leftX, lowerY + height);
    glEnd();

    glDisable(GL_BLEND);
}

void Graphics::drawRectFill(GLint leftX, GLint lowerY, GLint width, GLint height)
{
    glEnable(GL_BLEND);

    // Look at Ch. 6, pg. 234 Red Book for other options.
    // The bigger the source alpha channel, the more opaque it becomes.
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glBegin(GL_TRIANGLE_STRIP);
    // lower-left
    glVertex2i(leftX, lowerY);
    // lower-right
    glVertex2i(leftX + width, lowerY);
    // upper-left
    glVertex2i(leftX, lowerY + height);
    // upper-right
    glVertex2i(leftX + width, lowerY + height);
    glEnd();

    glDisable(GL_BLEND);
}

void Graphics::setThickness(GLfloat thickness)
{
    glLineWidth(thickness);
}

void Graphics::setColor(float red, float green, float blue, float alpha)
{
    glColor4f(red, green, blue, alpha);
    // Call a glRasterPos* after color change to apply to pixmaps and bitmaps.
    glRasterPos2i(0, 0); // If this ever changed, boy will there be bugs.
}

void Graphics::setFontSize(uint32_t size)
{
    font.FaceSize(size);
}

bool Graphics::loadTexture(const char *inFile, Texture *outTexture)
{
    outTexture->name = 0;
    outTexture->width = 0;
    outTexture->height = 0;

    SDL_RWops *file = SDL_RWFromFile(inFile, "rb");

    if (file == NULL)
    {
        cerr << "Unable to load file: " << SDL_GetError() << endl;
        return false;
    }

    SDL_Surface *image = IMG_Load_RW(file, 0);

    // Release the file resource if need be.
    if (file)
    {
        SDL_FreeRW(file);
    }

    if (image == NULL)
    {
        cerr << "Unable to load image: " << IMG_GetError() << endl;
        return false;
    }

    // Umm... We're just going to assume the texture will be GL_RGBA for now.
    // Look at http://gpwiki.org/index.php/SDL:Tutorials:Using_SDL_with_OpenGL
    // for more information.

    // Create a GL handle for the 1 texture.
    GLuint textures[1];
    glGenTextures(1, textures); // pg. 415 of Red Book 6th ed.

    // glBindTexture: Any texture calls from now on refer to this texture.
    glBindTexture(GL_TEXTURE_2D, textures[0]);

    // glTexParameter pg. 432 of Red Book 6th ed.
    // Possible other filters: Table 9-3 (pg. 412 of Red Book 6th ed.)
    // GL_LINEAR is smoother and generally more pleasing than GL_NEAREST.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);


    // Alter the way the image is loaded into video memory depending on how
    // bits it has: usually this is 8, 24, or 32 bits.
    Uint8 bpp = image->format->BitsPerPixel;

    uint32_t inputFormat = GL_RGBA;
    uint32_t outputFormat = GL_COMPRESSED_RGBA;
    switch (image->format->BitsPerPixel)
    {
    case 8:
        inputFormat = GL_ALPHA;
        outputFormat = GL_COMPRESSED_ALPHA;
        break;
    case 24:
        inputFormat = GL_RGB;
        outputFormat = GL_COMPRESSED_RGB;
        break;
    case 32:
        inputFormat = GL_RGBA;
        outputFormat = GL_COMPRESSED_RGBA;
        break;
    default:
        cerr << "Bits per pixel in the loading image wasn't handled." << endl;
        cerr << "\tImage File: " << inFile << " bpp: " << 0 + bpp << endl;
        inputFormat = GL_RGBA;
    }

    // We are loading a 2D image with 1 mipmap whose internal storage may be
    // compressed RGBA. The format coming into it is an uncompressed RGBA format.
    // Ch. 9 pg. 380 Red Book 6th ed.

    glTexImage2D(GL_TEXTURE_2D, 0, outputFormat, image->w, image->h, 0,
            inputFormat, GL_UNSIGNED_BYTE, image->pixels);

    // TODO check to see if it really was compressed and its memory savings by
    // using the method outlined in Ch. 9 pg. 397 Red Book 6th ed.

    outTexture->name = textures[0];
    outTexture->width = image->w;
    outTexture->height = image->h;

    // We loaded the contents of image into GL memory. image is no longer needed
    SDL_FreeSurface(image);

    return true;
}

/**
 * @returns true if successfully created all graphics for the game, or outputs
 * an error and returns false if not.
 */
bool Graphics::initGraphics()
{
    /// @todo change "res/" to a variable
    bool result = loadTexture("res/o.png", &textureO);
    result &= loadTexture("res/x.png", &textureX);
    result &= loadTexture("res/line.png", &textureLine);
    result &= loadTexture("res/legalpad.jpg", &textureBack);
    result &= loadTexture("res/you.png", &textureYou);
    result &= loadTexture("res/win.png", &textureWin);
    result &= loadTexture("res/lose.png", &textureLose);
    result &= loadTexture("res/cat.png", &textureCat);
    result &= loadTexture("res/title.png", &textureTitle);
    result &= loadTexture("res/pezaddrawn.png", &texturePezad);

    return result;
}

/**
 * @returns true if successfully created an OpenGL-based window, or outputs
 * an error and returns false if not.
 */
bool Graphics::initGL()
{
    // OpenGL settings need adjusted before creating the window.
    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

    // Create the window with OpenGL.
    SDL_Surface *screen = SDL_SetVideoMode(640, 480, 32, SDL_OPENGL);
    if (screen == NULL)
    {
        cerr << "Could not create OpenGL window: " << SDL_GetError() << endl;
        SDL_Quit();
        return false;
    }
    glViewport(0, 0, (GLsizei) 640, (GLsizei) 480);

    // We want to make the projection orthogonal.
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    // There will be no foreshortening. Objects will be the same size at any
    // distance. The depth-clipping occurs at -1.0 to 1.0.
    gluOrtho2D(0, 640, 0, 480); // lower-left is 0, 0. Upper-right is 640, 480

    // OpenGL origin is lower-left, images origins are upper-left. Flip y-axis.
    // TODO copy the image into the texture correctly so this can be removed,
    // and texture manipulation operations go easier.
    glMatrixMode(GL_TEXTURE);
    glLoadIdentity(); // It starts out as identity, but it is good habit.
    glScalef(1.0f, -1.0f, 1.0f);

    glMatrixMode(GL_MODELVIEW);

    glClearColor((GLclampf) .75, (GLclampf) .75, (GLclampf) .75, (GLclampf) 1);

    return true;
}

/**
 * @returns true if SDL was initialized correctly, or outputs an error and
 * 	returns false if not.
 */
bool Graphics::initSDL()
{
    // Initialize SDL video subsystem, which also inits other important systems.
    if (SDL_Init(SDL_INIT_VIDEO) != 0)
    {
        cerr << "SDL_Init Failed: " << SDL_GetError() << endl;
        return false;
    }

    // Set the window caption and icon.
    SDL_WM_SetCaption("Tic-tac-toe", '\0');

    SDL_Surface *icon = IMG_Load("res/icon.png");
    SDL_WM_SetIcon(icon, 0);
    SDL_FreeSurface(icon);

    return true;
}
